package com.ccc.netty.upload.client;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.codec.digest.DigestUtils;

import com.ccc.netty.upload.ByteUtils;
import com.ccc.netty.upload.enums.MsgType;

import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TcpClient {

	private Socket socket;

	public static void main(String[] args) {

		TcpClient client = new TcpClient();

		String basePath = "D:/test-upload/";

		// 经典phantomjs
		File file = new File(basePath + "phantomjs-2.1.1-windows.zip");

		if (!file.exists()) {
			log.error("文件不存在");
			return;
		}

		try {

			@Cleanup
			FileInputStream fin = new FileInputStream(file);

			@Cleanup
			FileChannel fc = fin.getChannel();

			// 文件总大小
			long totalSize = fc.size();

			// 分片文件大小 我们规定一片最多1024字节
			int chunkSize = 1024;

			// 分片总数
			int chunk = (int) ((totalSize / chunkSize) + ((totalSize % chunkSize) == 0 ? 0 : 1));

			// 文件唯一标识 文件名+ 时间 md5摘要16位
			byte[] md5 = ByteUtils.hexStr2Byte(DigestUtils.md5Hex(file.getName() + System.currentTimeMillis()));

			// 切片文件
//			chunkFile(Paths.get(file.getAbsolutePath()), chunkSize, md5);

			// 设定一个计数器
			CountDownLatch latch = new CountDownLatch(chunk);

			// 因为这里分片并不多 采用如果分片小于5则此线程池大小为chunk，大于5则为5
			ExecutorService fixedThreadPool = Executors.newFixedThreadPool(chunk >= 5 ? 5 : chunk);

			FileChannel fileChannel = FileChannel.open(Paths.get(file.getAbsolutePath()),
					EnumSet.of(StandardOpenOption.READ));

			// 每个分片最大1024，所以只有最后一片不一定是1024，但是也能算出来
			int lastSize = (int) (fileChannel.size() - ((chunk - 1) * 1024));// 总长度减去前片的和

			for (int chunkNum = 0; chunkNum < chunk; chunkNum++) {

				int size = chunkNum == chunk ? lastSize : chunkSize;

				ByteBuffer buffer = ByteBuffer.allocate(size);

				fileChannel.read(buffer);

				// [[消息类型x1][文件标识x16][分片序号x2][文件总大小x8][分片文件长度x2][分片文件数据][结束位x2(默认0xff)]]
				ByteBuffer msgBuffer = ByteBuffer.allocate(29 + size);

				msgBuffer.put(MsgType.UPLOAD.getType());// 消息类型x1

				msgBuffer.put(md5);// 文件标识x16

				msgBuffer.putShort((short) chunkNum);// 分片序号x2 【最多是65535片 因为只用了2个字节表示】

				msgBuffer.putLong(fileChannel.size());// 文件总大小x8 其实不用这么多大但是为了配合long直接用了8个字节 实际情况可以根据需要改成4个字节或者加算法使数据变小

				msgBuffer.putShort((short) size);// 分片文件长度x2 因为一个字节只有255 两个字节65535 这里最大1024 所以用两个字节

				msgBuffer.put(buffer.array());// 分片文件数据 最大1024 1024+45

				msgBuffer.putShort((short) 0xff);// 结束位FF

//				[91, -120, 111, 23, -11, 17, -19, 121, 8, 18, -114, 104, 82, 11, -118, -93]

				fixedThreadPool.execute(() -> {
					// 将组装好的报文，在线程池中创建线程运行
					client.sendMsgBySocket(msgBuffer);
					// 应该要判断是否成功的。这里不判断认为都成功
					latch.countDown();
				});

			}

			// 所有线程没发送完全之前 一直等待
			latch.await();

			fixedThreadPool.shutdown();

			// 发完了所有分片需要通知
			// 这里只是客户端继续给服务端发送一个消息,报文可以参考上传文件的格式，合并文件的方法可以看我上一篇文章

		} catch (Exception e) {
			log.error("[{}]", e);
		} finally {
			if (null != client.getSocket()) {
				try {
					client.getSocket().close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

	}

	private Socket getSocketClient() {

		if (null == this.getSocket()) {
			// 要连接的服务端IP地址和端口
			String host = "127.0.0.1";
			int port = 8888;

			// 与服务端建立连接
			try {
				this.socket = new Socket(host, port);
			} catch (IOException e) {
				log.error("{}", e);
			}
		}

		return this.getSocket();

	}

	/**
	 * TODO 本来发送是不需要返回值的，但是这里为了使用latch来计算发了多少，通过返回true来表示发送成功一个
	 * 
	 * @param msg
	 * @return
	 */
	synchronized public boolean sendMsgBySocket(ByteBuffer msg) {
		try {
//			Socket socket = getSocketClient();

			//这段代码仅仅调试用，尽量一个不要开多个socket跟服务端通信，应该使用同一个链路，多次发送消息，服务端解决粘包和拆包的问题
			// 要连接的服务端IP地址和端口-----------------------------------
			String host = "127.0.0.1";
			int port = 8888;

			// 与服务端建立连接
			this.socket = new Socket(host, port);
			//---------------------------------------------------------

			// 建立连接后获得输出流
			OutputStream outputStream = socket.getOutputStream();
			byte[] msgArray = msg.array();
			// 报文头start-----------------------------------------------
			// 55表示协议版本
			short protocol = 0x55;
			ByteBuffer header = ByteBuffer.allocate(4);
			header.putShort(protocol);// 版本x2

			// 写入本次报文总长度 固定是37 + 分片文件长度
			short length = (short) (msgArray.length);
			header.putShort(length);// 报文总长度x2
			// 报文头end-----------------------------------------------
			outputStream.write(header.array());
			outputStream.write(msgArray);
			outputStream.flush();

			InputStream inputStream = socket.getInputStream();
			byte[] bytes = new byte[1024];
			int read = 0;

			while (true) {
				// 死循环直到服务器返回 这里可以添加超时机制，防止服务器出问题后无限循环
				read = inputStream.read(bytes);
				if (read > 0) {
					String result = new String(bytes);
					log.info("receive : {}", result);
					if ("ok".equals(result)) {
						// 假定回复ok就是成功了
						return true;
					}
					break;
				}
			}

		} catch (Exception e) {
			log.error("[{}]", e);
		}

		return false;

	}

	/**
	 * 对文件按照指定大小进行分片，在文件所在目录生成分片后的文件块儿
	 * 
	 * @param file
	 * @param chunkSize
	 * @throws IOException
	 */
	public static void chunkFile(Path file, long chunkSize, String fileName) throws IOException {
		if (Files.notExists(file) || Files.isDirectory(file)) {
			throw new IllegalArgumentException("文件不存在:" + file);
		}
		if (chunkSize < 1) {
			throw new IllegalArgumentException("分片大小不能小于1个字节:" + chunkSize);
		}
		// 原始文件大小
		final long fileSize = Files.size(file);
		// 分片数量
		final long numberOfChunk = fileSize % chunkSize == 0 ? fileSize / chunkSize : (fileSize / chunkSize) + 1;
		// 读取原始文件
		try (FileChannel fileChannel = FileChannel.open(file, EnumSet.of(StandardOpenOption.READ))) {
			for (int i = 0; i < numberOfChunk; i++) {
				long start = i * chunkSize;
				long end = start + chunkSize;
				if (end > fileSize) {
					end = fileSize;
				}
				// 分片文件名称
				Path chunkFile = Paths.get(fileName + "-" + i);
				try (FileChannel chunkFileChannel = FileChannel.open(file.resolveSibling(chunkFile),
						EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))) {
					// 返回写入的数据长度
					fileChannel.transferTo(start, end - start, chunkFileChannel);
				}
			}
		}
	}

	public Socket getSocket() {
		return socket;
	}

	public void setSocket(Socket socket) {
		this.socket = socket;
	}

}
